/*
 * Copyright 2011 Clean GWT
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.cleangwt.uibinder.rebind;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.util.StringUtils;

import com.cleangwt.json.client.SimpleObject;
import com.cleangwt.uibinder.client.RequiresFormRebind;
import com.cleangwt.uibinder.client.UiInput;
import com.cleangwt.uibinder.client.UiInputAddon;
import com.cleangwt.validation.client.Validation;
import com.cleangwt.validation.client.ValidationMessages;
import com.cleangwt.validation.client.Validator;
import com.cleangwt.validation.client.constraints.Constraint;
import com.cleangwt.validation.client.constraints.relative.RelativeConstraint;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.user.client.TakesValue;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * A writer for generating implementation methods of {@link RequiresFormRebind}.
 * 
 * @author Jess
 * @version 1.0
 * @since 1.0
 */
public class FormRebindWriter {
  private final TreeLogger logger;
  private final List<JField> inputFields;

  public FormRebindWriter(TreeLogger logger, TypeOracle oracle, JClassType binderType)
      throws UnableToCompleteException {
    JClassType takesValueType = oracle.findType(TakesValue.class.getCanonicalName());
    this.logger = logger;
    this.inputFields = this.findInputFields(binderType, takesValueType);
  }

  /**
   * Adds imports.
   * 
   * @param composer the composer
   */
  public void addImports(ClassSourceFileComposerFactory composer) {
    composer.addImport(SimpleObject.class.getCanonicalName());
    composer.addImport(Validator.class.getCanonicalName());
    composer.addImport(ValidationMessages.class.getCanonicalName());
    for (JField field : inputFields) {
      composer.addImport(field.getType().getQualifiedSourceName());
    }

    for (JField field : inputFields) {
      List<Annotation> annotations = findConstraintAnnotations(field);
      if (annotations.isEmpty()) {
        continue;
      }
      for (Annotation an : annotations) {
        Class<? extends Annotation> type = an.annotationType();
        Constraint c = type.getAnnotation(Constraint.class);
        Class<? extends Validation> validator = c.validatedBy();
        composer.addImport(validator.getCanonicalName());
      }
    }

    for (JField field : inputFields) {
      List<Annotation> annotations = findRelativeConstraintAnnotations(field);
      if (annotations.isEmpty()) {
        continue;
      }
      for (Annotation an : annotations) {
        Class<? extends Annotation> type = an.annotationType();
        RelativeConstraint c = type.getAnnotation(RelativeConstraint.class);
        Class<? extends Validation> validator = c.validatedBy();
        composer.addImport(validator.getCanonicalName());
      }
    }
  }

  /**
   * Generates implementation methods.
   * 
   * @param sw the source writer
   * @param binderType the binder type
   * @throws UnableToCompleteException
   */
  public void run(SourceWriter sw) throws UnableToCompleteException {
    createSetFormData(sw);
    sw.println();
    createGetFormData(sw);
    sw.println();
    createAddConstraints(sw);
  }

  private void createSetFormData(SourceWriter sw) {
    sw.println("public void setFormData(SimpleObject value){");
    sw.indent();
    sw.println("if(value == null){");
    sw.indent();
    sw.println("value = new SimpleObject();");
    sw.outdent();
    sw.println("}");
    for (JField field : inputFields) {
      UiInput uiEditor = field.getAnnotation(UiInput.class);
      String name = uiEditor.name();
      String value = uiEditor.value();
      if ("".equals(name)) {
        name = field.getName();
      }
      if ("".equals(value)) {
        value = "value";
      }
      String setter;
      JClassType fieldType = field.getType().isClassOrInterface();
      Set<? extends JClassType> fieldSuperTypes = fieldType.getFlattenedSupertypeHierarchy();
      String castValueType = null;
      setter = "set" + StringUtils.capitalize(value);
      // find setter without specifying a parameter type
      for (JClassType type : fieldSuperTypes) {
        for (JMethod m : type.getMethods()) {
          // there should be one setter with one parameter matched
          if (setter.equals(m.getName()) && m.getParameterTypes().length == 1) {
            castValueType = m.getParameterTypes()[0].getSimpleSourceName();
            break;
          }
        }
      }
      // sw.indent();
      String[] wrapper = name.split("\\.");
      if (wrapper.length > 1) {
        String cascaded = "";
        String storeName = wrapper[wrapper.length - 1];
        String cascadedGetter = "";
        for (int i = 0; i < wrapper.length - 1; i++) {
          cascadedGetter += (".getSimpleObject(\"" + wrapper[i] + "\")");
        }
        if (cascaded.length() > 0) {
          cascaded = cascaded.substring(0, cascaded.length() - 1);
        }
        // add try-catch to avoid null point exception from retriever
        sw.println("try{");
        sw.indent();
        sw.println("%s.%s((%s)value%s.get(\"%s\"));", field.getName(), setter, castValueType,
            cascadedGetter, storeName);
        sw.outdent();
        sw.println("}catch(NullPointerException e){");
        sw.indent();
        sw.println("%s.setValue(null);", field.getName());
        sw.outdent();
        sw.println("}");
      } else {
        if (Date.class.getSimpleName().equals(castValueType)) {
          // ensure the date value back
          sw.println("%s.%s(value.getDate(\"%s\"));", field.getName(), setter, name);
        } else {
          // cast to type
          sw.println("%s.%s((%s)value.get(\"%s\"));", field.getName(), setter, castValueType, name);
        }
      }

    }
    sw.outdent();
    sw.println("}");
  }

  private void createGetFormData(SourceWriter sw) {
    Set<String> ensuredNoneNullObjects = new HashSet<String>();
    sw.println("public SimpleObject getFormData(SimpleObject value){");
    sw.indent();
    sw.println("if(value == null){");
    sw.indent();
    sw.println("value = new SimpleObject();");
    sw.outdent();
    sw.println("}");
    for (JField field : inputFields) {
      UiInput uiEditor = field.getAnnotation(UiInput.class);
      String name = uiEditor.name();
      String value = uiEditor.value();
      if ("".equals(name)) {
        name = field.getName();
      }
      if ("".equals(value)) {
        value = "value";
      }
      String retriever;

      UiInputAddon extra = field.getAnnotation(UiInputAddon.class);
      retriever = "get" + StringUtils.capitalize(value);
      String[] wrapper = name.split("\\.");
      String storeName = wrapper[wrapper.length - 1];
      if (wrapper.length > 1) {
        String wrappedModel = "";
        for (int i = 0; i < wrapper.length - 1; i++) {
          String ensureGetter = "";
          for (int j = 0; j < i; j++) {
            ensureGetter += ".getSimpleObject(\"" + wrapper[j] + "\")";
          }
          String ensuredId = ensureGetter + "." + wrapper[i];
          if (!ensuredNoneNullObjects.contains(ensuredId)) {
            ensuredNoneNullObjects.add(ensuredId);
            sw.println("if(value%s.getSimpleObject(\"%s\") == null){", ensureGetter, wrapper[i]);
            sw.indent();
            sw.println("value%s.put(\"%s\",new SimpleObject());", ensureGetter, wrapper[i]);
            sw.outdent();
            sw.println("}");
          }
          wrappedModel += (".getSimpleObject(\"" + wrapper[i] + "\")");
        }
        sw.println("value%s.put(\"%s\",%s.%s());", wrappedModel, storeName, field.getName(),
            retriever);
      } else {
        sw.println("value.put(\"%s\",%s.%s());", name, field.getName(), retriever);
      }
      // store extra value
      if (extra != null) {
        String[] values = extra.value();
        String[] names = extra.name();
        if (values.length != names.length) {
          throw new IllegalArgumentException("Illegal @UiInputAddon at " + field.getName());
        }
        for (int i = 0; i < values.length; i++) {
          String v = StringUtils.capitalize(values[i]);
          sw.println("value.put(\"%s\",%s.get%s());", names[i], field.getName(), v);
        }
      }
    }
    sw.println("return value;");
    sw.outdent();
    sw.println("}");
  }

  private void createAddConstraints(SourceWriter sw) {
    sw.println("public void addConstraints(Validator validator){");
    sw.indent();
    sw.println("assert validator != null :\"%s\";", " The validator must not be null.");
    // add constraints
    for (JField field : inputFields) {
      List<Annotation> annotations = findConstraintAnnotations(field);
      if (annotations.isEmpty()) {
        continue;
      }
      sw.println("validator.addContext(%s)", field.getName());
      for (Annotation an : annotations) {
        Class<? extends Annotation> type = an.annotationType();
        Constraint c = type.getAnnotation(Constraint.class);
        Class<? extends Validation> validator = c.validatedBy();
        String validatorName = validator.getSimpleName();
        String[] args = c.args();
        // constructor args builder
        sw.println(".add(new %s(%s))", validatorName, this.prepareConstructorArgs(an, args));
      }
      sw.println(";");
    }
    // add relative constraints
    sw.println();
    for (JField field : inputFields) {
      List<Annotation> annotations = this.findRelativeConstraintAnnotations(field);
      if (annotations.isEmpty()) {
        continue;
      }

      for (Annotation an : annotations) {
        Class<? extends Annotation> type = an.annotationType();
        RelativeConstraint c = type.getAnnotation(RelativeConstraint.class);
        Class<? extends Validation> validator = c.validatedBy();
        String validatorName = validator.getSimpleName();
        String[] relatives = c.relatives();
        String[] args = c.args();
        try {
          String[] relativeFields = new String[relatives.length];
          int i = 0;
          for (String relative : relatives) {
            Method m = type.getMethod(relative, new Class[] {});
            relativeFields[i++] = (String) m.invoke(an, new Object[] {});
          }
          StringBuilder constraintFields = new StringBuilder(field.getName());
          for (String relative : relativeFields) {
            constraintFields.append(",").append(relative);
          }
          sw.println("validator.addContext(%s).add(new %s(%s));", constraintFields.toString(),
              validatorName, this.prepareConstructorArgs(an, args));
        } catch (Exception e) {
          logger.log(TreeLogger.ERROR, an.getClass().getSimpleName() + " Error", e);
        }

      }
    }

    sw.outdent();
    sw.println("}");
  }

  private static Object getAnnotationProperty(Annotation annotaion, String name) throws Exception {
    Class<? extends Annotation> type = annotaion.annotationType();
    Method m = type.getMethod(name, new Class[] {});
    return asLiteral(m.invoke(annotaion, new Object[] {}));
  }

  private static String resolveInnerArgs(Annotation a, String[] propertyNames) throws Exception {
    if (propertyNames == null) {
      return "";
    }
    StringBuilder sb = new StringBuilder();
    boolean first = true;
    for (String arg : propertyNames) {
      if (first) {
        first = false;
      } else {
        sb.append(",");
      }
      String prop = (String) getAnnotationProperty(a, arg);
      if (prop.startsWith("{") && prop.endsWith("}")) {
        prop = prop.substring(1, prop.length() - 1) + "()";
      }
      sb.append(prop);
    }
    return sb.toString();
  }

  private String prepareConstructorArgs(Annotation constraint, String[] args) {
    StringBuilder sb = new StringBuilder();
    for (String arg : args) {
      try {
        if (arg.startsWith("message")) {
          String message = (String) getAnnotationProperty(constraint, "message");
          // use predefined message
          if (message.startsWith("{") && message.endsWith("}")) {
            String msgCreator = message.substring(1, message.length() - 1);
            int s = arg.indexOf("(");
            int e = arg.indexOf(")");
            // may with inner args
            if (s > 0 && e > 0) {
              String[] innerArgs = arg.substring(s + 1, e).split(",");
              sb.append(msgCreator + "(" + resolveInnerArgs(constraint, innerArgs) + "),");
            } else {
              sb.append(msgCreator + "(),");
            }
          } else {
            sb.append(message + ",");
          }
        } else {
          sb.append(getAnnotationProperty(constraint, arg) + ",");
        }
      } catch (Exception e) {
        logger.log(TreeLogger.ERROR, constraint.getClass().getName() + " constraint", e);
      }
    }

    return sb.substring(0, sb.length() - 1);
  }

  private List<Annotation> findConstraintAnnotations(JField field) {
    List<Annotation> list = new ArrayList<Annotation>();
    Annotation[] annotations = field.getDeclaredAnnotations();
    for (Annotation a : annotations) {
      Class<? extends Annotation> type = a.annotationType();
      if (type.isAnnotationPresent(Constraint.class)) {
        list.add(a);
      }
    }
    return list;
  }

  private List<Annotation> findRelativeConstraintAnnotations(JField field) {
    List<Annotation> list = new ArrayList<Annotation>();
    Annotation[] annotations = field.getDeclaredAnnotations();
    for (Annotation a : annotations) {
      Class<? extends Annotation> type = a.annotationType();
      if (type.isAnnotationPresent(RelativeConstraint.class)) {
        list.add(a);
      }
    }
    return list;
  }

  private List<JField> findInputFields(JClassType binderType, JClassType takesValueType)
      throws UnableToCompleteException {
    List<JField> fields = new ArrayList<JField>();
    for (JField field : binderType.getFields()) {
      if (field.isAnnotationPresent(UiInput.class)) {
        JClassType fieldType = field.getType().isClassOrInterface();
        if (fieldType.isAssignableTo(takesValueType)) {
          fields.add(field);
        }
      }
    }
    return fields;
  }

  /**
   * Returns the literal value of an object that is suitable for inclusion in Java Source code.
   * 
   * <p>
   * Supports all types that {@link Annotation} value can have.
   * 
   * 
   * @throws IllegalArgumentException if the type of the object does not have a java literal form.
   */
  private static String asLiteral(Object value) throws IllegalArgumentException {
    Class<?> clazz = value.getClass();
    JProgram jProgram = new JProgram();

    if (clazz.isArray()) {
      StringBuilder sb = new StringBuilder();
      Object[] array = (Object[]) value;

      sb.append("new " + clazz.getComponentType().getCanonicalName() + "[] ");
      sb.append("{");
      boolean first = true;
      for (Object object : array) {
        if (first) {
          first = false;
        } else {
          sb.append(",");
        }
        sb.append(asLiteral(object));
      }
      sb.append("}");
      return sb.toString();
    }

    if (value instanceof Boolean) {
      return jProgram.getLiteralBoolean(((Boolean) value).booleanValue()).toSource();
    } else if (value instanceof Byte) {
      return jProgram.getLiteralInt(((Byte) value).byteValue()).toSource();
    } else if (value instanceof Character) {
      return jProgram.getLiteralChar(((Character) value).charValue()).toSource();
    } else if (value instanceof Class<?>) {
      return ((Class<?>) ((Class<?>) value)).getCanonicalName() + ".class";
    } else if (value instanceof Double) {
      return jProgram.getLiteralDouble(((Double) value).doubleValue()).toSource();
    } else if (value instanceof Enum) {
      return value.getClass().getCanonicalName() + "." + ((Enum<?>) value).name();
    } else if (value instanceof Float) {
      return jProgram.getLiteralFloat(((Float) value).floatValue()).toSource();
    } else if (value instanceof Integer) {
      return jProgram.getLiteralInt(((Integer) value).intValue()).toSource();
    } else if (value instanceof Long) {
      return jProgram.getLiteralLong(((Long) value).intValue()).toSource();
    } else if (value instanceof String) {
      String v = (String) value;
      // if value is specified for message
      if (v.startsWith("{") && v.endsWith("}")) {
        return v;
      } else {
        return '"' + Generator.escape((String) value) + '"';
      }
    } else {
      throw new IllegalArgumentException(value.getClass()
          + " can not be represented as a Java Literal.");
    }
  }

}
